Cordapp 開発Tips集
1. Extending and Overriding Flow
Flowのextendとoverrideは Corda 4から可能になった、これによってthird-party CorDappsが使えるようになった
Extend可能なBase Flowを作る
以下のようにopen classとしてextend可能に実装する
また、open functionとして実装する
code:kotlin
@InitiatingFlow
open class SendMessageFlow(private val message: MessageState) :
FlowLogic<SignedTransaction>() {
open fun preTransactionBuild() {
// to be implemented by sub type flows - otherwise do nothing
}
open fun preSignaturesCollected(transaction: SignedTransaction) {
// to be implemented by sub type flows - otherwise do nothing
}
open fun postSignaturesCollected(transaction: SignedTransaction) {
// to be implemented by sub type flows - otherwise do nothing
}
open fun postTransactionCommitted(transaction: SignedTransaction) {
// to be implemented by sub type flows - otherwise do nothing
}
@Suspendable
final override fun call(): SignedTransaction {
logger.info("Started sending message ${message.contents}")
preTransactionBuild()
val tx = verifyAndSign(transaction())
preSignaturesCollected(tx)
val sessions = listOf(initiateFlow(message.recipient))
val stx = collectSignature(tx, sessions)
postSignaturesCollected(stx)
return subFlow(FinalityFlow(stx, sessions)).also {
logger.info("Finished sending message ${message.contents}")
postTransactionCommitted(it)
}
}
// collectSignature
// verifyAndSign
// transaction
}
Initiating FlowのExtend
Responder FlowのExtend
Responder FlowのOverride (Replace)
2. Responder Flow Validation
問題意識
ResponderFlowは基本的に来たTXに署名して返すという処理をするので、自分にとって「ビジネス的に都合が悪い」ものに自動的に合意してしまう
自分に関係ないけど、requiredSignerに入れられたら署名してしまう
Responder Flow での validation方法
必須前提知識 : Contract は Signature Constraintsがあるので、配布されたjarファイル以外は基本的には使えない(変更できない)が、Flow のcodeは自由に変更可能
以下のような感じで Responder Flowの checkTransactionにvalidationを記述するのがbest practiceぽい
code:kotlin
@InitiatedBy(SendMessageFlow::class)
class SendMessageResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val stx = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {
val message = stx.coreTransaction.outputStates.single() as MessageState
check(message.recipient == ourIdentity) { "I think you got the wrong person" }
check(!message.contents.containsSwearWords()) { "Mind your language" }
check(!message.contents.containsMemes()) { "Only serious messages are accepted" }
check(message.sender.name.organisation != "Nigerian Prince") { "Spam message detected" }
}
})
return subFlow(ReceiveFinalityFlow(otherSideSession = session, expectedTxId = stx.id))
}
}
SignTransactionFlowの checkTransactionをoverrideしている形だが、先にContractによるvalidationであるLedgerTransaciton.verifyが実行される
つまり、Contract による形式的かつネットワーク全体で共通のvalidationを行なった後に、独自のvalidationとして checkTransactionを実行する
requireThat usingを使っても良い
code:kotlin
requireThat {
// <message if criteria fails> using <statement that must be true>
"I think you got the wrong person" using (message.recipient == ourIdentity)
// Non using lines can also be included
val contents = message.contents
"Mind your language" using (!contents.containsSwearWords())
"Only serious messages are accepted" using (!contents.containsMemes())
"Spam message detected" using (message.sender.name.organisation != "Nigerian Prince")
}
ResponderFlowとContractの使い分け
Responder Flowの checkTransactionでvalidationするべき項目は以下の3つが考えられる
1. Identity
基本的に自身のIdentityにはContractからアクセスできないため、Flowの中で実行する
2. VaultQueryを必要とするvalidation
ContractからvaultQueryできないため
3. ビジネスロジック
例えばvalueやamountなどの値が「80なら取引するが100なら取引しない」などのvalidationはネットワーク共通のContractではできないため
Responder flow validation 実装のベストプラクティス
Cordapp実装して配るマンの開発時注意点
以下のようにResponderFlowは継承可能な形、checkTransaction functionはabstractに実装しよう
code:kotlin
@InitiatedBy(SendMessageFlow::class)
// ResponderFlow は openにしておく(Javaの場合はfinalを付けないようにする)
open class SendMessageResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
// override可能な関数定義
open fun checkTransaction(stx: SignedTransaction) {
// To be implemented by sub type flows - otherwise do nothing
}
@Suspendable
// callはoverrideされないようにfinalにしておく
final override fun call(): SignedTransaction {
val stx = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {
// The validation function is called
this@SendMessageResponder.checkTransaction(stx)
// Any other rules the CorDapp developer wants executed
}
})
return subFlow(ReceiveFinalityFlow(otherSideSession = session, expectedTxId = stx.id))
}
}
↑のような実装にしておくことで、各Nodeで SendMessageResponderを継承し、abstract だったcheckTransactionをoverrideしてvalidation logicを実装する、つまりFlowをカスタマイズすることができるよね
code:kotlin
@InitiatedBy(SendMessageFlow::class)
class ValidatingSendMessageResponder(session: FlowSession) : SendMessageResponder(session) {
override fun checkTransaction(stx: SignedTransaction) {
val message = stx.coreTransaction.outputStates.single() as MessageState
check(message.recipient == ourIdentity) { "I think you got the wrong person" }
check(!message.contents.containsSwearWords()) { "Mind your language" }
check(!message.contents.containsMemes()) { "Only serious messages are accepted" }
check(message.sender.name.organisation != "Nigerian Prince") { "Spam message detected" }
}
}
もしCordapp開発者がResponderFlowを継承不可の実装にしてたら...
各Nodeは from scratchで以下のようにResponderクラス実装することになるよ
code:kotlin
@InitiatedBy(SendMessageFlow::class)
class ValidatingSendMessageResponder(private val session: FlowSession) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val stx = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) {
val message = stx.coreTransaction.outputStates.single() as MessageState
check(message.recipient == ourIdentity) { "I think you got the wrong person" }
check(!message.contents.containsSwearWords()) { "Mind your language" }
check(!message.contents.containsMemes()) { "Only serious messages are accepted" }
check(message.sender.name.organisation != "Nigerian Prince") { "Spam message detected" }
}
})
return subFlow(ReceiveFinalityFlow(otherSideSession = session, expectedTxId = stx.id))
}
}
node.confに以下のような設定を書くことで SendMessageFlowに対するResponderFlowはSendMessageResponderではなくValidatingSendMessageResponderになるようにoverrideできるよ
code:node.conf
flowOverrides {
overrides=[
{
initiator="dev.lankydan.tutorial.flows.SendMessageFlow"
responder="dev.lankydan.tutorial.flows.ValidatingSendMessageResponder"
}
]
}
Responder Flow設定のdocumentはこちら 2. Common CorDapp mistakes and how to fix them
WIP
3. Web3j Corda SDK
4. Truffle’s Corda Flavored Ganache
5. Notary's vault
InputStateが存在しないTransactionの場合は、NotaryにTX送信されない (validatingもnon-validatingも)
NotaryはInputStateの二重支払いチェックをするNodeだから
この特性により、InputStateが存在しないTransactionだが、validating-Notaryにもcordapp.jarによるvalidationをしてもらいたいというような設計は不可能
Notaryの持つ 二重支払いチェック用のtableは、通常のNodeがStateを保存しにいく先のtableとは異なる
validating Notaryの場合は、Stateを保存するtableも通常通り機能するが、non-validatingの場合はそちらのtableは空になる
Notaryの二重支払い用のtableとカラムは以下のようになる。
NODE_NETWORK_PARAMETERS : Network parametersを保持しておくtable
HASH
EPOCH
PARAMETERS_BYTES
SIGNATURE_BYTES
CERT
PARENT_CERT_PATH
NODE_NOTARY_COMMITTED_STATES : 消費したStateに関する情報を記録しておくtable
OUTPUT_INDEX : TRANSACTION_IDの何番目のOutputを消費したか
TRANSACTION_ID : 消費したStateを含むTXのID
CONSUMING_TRANSACTION_ID : 消費したStateをInputに、新たなStateをOutputにしたTXのID
NODE_NOTARY_COMMITTED_TXS : 新たに作成されたCONSUMING_TRANSACTION_IDを記録しておくTable
TRANSACTION_ID : A4C3A394B273322E56A99E7303A43B11E9D8EE915C7901E40F982702
shuntak.icon < NODE_NOTARY_COMMITTED_STATESがあれば十分なので、存在意義がよくわからないテーブル
NODE_NOTARY_REQUEST_LOG : NodeからNotaryに送られてきたrequestのログ
ID
CONSUMING_TRANSACTION_ID
REQUESTING_PARTY_NAME
REQUEST_TIMESTAMP
REQUEST_SIGNATURE
6. Attachmentを用いたFlowとContract
7. Flow Design Guidelines
Reference